--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

function gadget:GetInfo()
  return {
    name      = "Teleport",
    desc      = "Teleport spell",
    author    = "CarRepairer",
    date      = "2009-07-25",
    license   = "GNU GPL, v2 or later",
    layer     = 0,
    enabled   = true  --  loaded by default?
  }
end

if (not gadgetHandler:IsSyncedCode()) then
  return false -- no unsynced code
end

Spring.SetGameRulesParam("teleport",1)

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--
--  Proposed Command ID Ranges:
--
--    all negative:  Engine (build commands)
--       0 -   999:  Engine
--    1000 -  9999:  Group AI
--   10000 - 19999:  LuaUI
--   20000 - 29999:  LuaCob
--   30000 - 39999:  LuaRules
--

local CMD_TELEPORT = 38522

local TELEPORT_CEG = 'BANISHERTRAIL'

local Spring      = Spring
local MoveCtrl    = Spring.MoveCtrl
local coroutine   = coroutine
local Sleep       = coroutine.yield
local pairs       = pairs
local assert      = assert
local ipairs      = ipairs

local pi2    = math.pi*2
local random = math.random

local CMD_STOP = CMD.STOP

local spGetHeadingFromVector = Spring.GetHeadingFromVector
local spGetUnitBasePosition  = Spring.GetUnitBasePosition
local spInsertUnitCmdDesc  = Spring.InsertUnitCmdDesc
local spSetUnitRulesParam  = Spring.SetUnitRulesParam
local spSetUnitNoMinimap   = Spring.SetUnitNoMinimap
local spGetCommandQueue    = Spring.GetCommandQueue
local spGiveOrderToUnit    = Spring.GiveOrderToUnit
local spSetUnitNoSelect    = Spring.SetUnitNoSelect
local spSetUnitBlocking    = Spring.SetUnitBlocking
local spSetUnitMoveGoal    = Spring.SetUnitMoveGoal
local spGetGroundHeight    = Spring.GetGroundHeight
local spTestBuildOrder     = Spring.TestBuildOrder
local spGetGameSeconds     = Spring.GetGameSeconds
local spGetUnitHeading     = Spring.GetUnitHeading
local spSetUnitNoDraw      = Spring.SetUnitNoDraw
local spCallCOBScript      = Spring.CallCOBScript
local spSetUnitNoDraw      = Spring.SetUnitNoDraw
local spGetUnitDefID       = Spring.GetUnitDefID
local spGetUnitTeam        = Spring.GetUnitTeam
local spDestroyUnit        = Spring.DestroyUnit
local spCreateUnit         = Spring.CreateUnit
local spSpawnCEG           = Spring.SpawnCEG

local mcSetRotationVelocity = MoveCtrl.SetRotationVelocity
local mcSetLeaveTracks      = MoveCtrl.SetLeaveTracks
local mcSetPosition         = MoveCtrl.SetPosition
local mcSetRotation         = MoveCtrl.SetRotation
local mcDisable             = MoveCtrl.Disable
local mcEnable              = MoveCtrl.Enable

local emptyTable = {}

local coroutines  = {}
local lastTeleport    = {}
local landBoxSize = 60
local teleports       = {}
local teleporting     = {}

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

local teleportDefNames  = VFS.Include"LuaRules/Configs/teleport_defs.lua"

local teleportDefs = {}
for name, data in pairs(teleportDefNames) do
  teleportDefs[UnitDefNames[name].id] = data
end


local teleportCmdDesc = {
  id      = CMD_TELEPORT,
  type    = CMDTYPE.ICON_MAP,
  name    = 'Teleport',
  cursor  = 'Move',  -- add with LuaUI?
  action  = 'teleport',
  tooltip = 'Teleport to selected position.',
}

  
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

local function GetDist3(a, b)
  local x, y, z = (a[1] - b[1]), (a[2] - b[2]), (a[3] - b[3])
  return (x*x + y*y + z*z)^0.5
end

local function GetDist2Sqr(a, b)
  local x, z = (a[1] - b[1]), (a[3] - b[3])
  return (x*x + z*z)
end

local function Approach(unitID, cmdParams, range)
  spSetUnitMoveGoal(unitID, cmdParams[1],cmdParams[2],cmdParams[3], range)
end

local function StartScript(fn)
  local co = coroutine.create(fn)
  coroutines[#coroutines + 1] = co
end

local function ReloadQueue(unitID, queue, cmdTag)
  if (not queue) then
    return
  end

  --// remove finished command
  local start = 1
  if (queue[1])and(cmdTag == queue[1].tag) then start = 2 end

  spGiveOrderToUnit(unitID, CMD_STOP, emptyTable, emptyTable)
  for i=start,#queue do
    local cmd = queue[i]
    local cmdOpt = cmd.options
    local opts = {"shift"} -- appending
    if (cmdOpt.alt)   then opts[#opts+1] = "alt"   end
    if (cmdOpt.ctrl)  then opts[#opts+1] = "ctrl"  end
    if (cmdOpt.right) then opts[#opts+1] = "right" end
    spGiveOrderToUnit(unitID, cmd.id, cmd.params, opts)
  end
end



local function Teleport(unitID, goal, cmdTag, x, y, z)
	teleporting[unitID] = true
	local unitDefID     = spGetUnitDefID(unitID)
	local teleportDef   = teleportDefs[unitDefID]
	local reloadTime    = (teleportDef.reload or 0)*30
	lastTeleport[unitID] = spGetGameSeconds()
	spSetUnitRulesParam(unitID,"teleportReload",0)
	local function Reloadloop()
		local reloadTimeInv = 1/reloadTime
		for i=1, reloadTime do
		  spSetUnitRulesParam(unitID,"teleportReload",i*reloadTimeInv)
		  Sleep()
		end
	end
	goal[2] = spGetGroundHeight(goal[1],goal[3])

	local cegcount = 15
	for i=0,cegcount do
		local x1 = (x*i + goal[1]*(cegcount-i))/cegcount
		local y1 = (y*i + goal[2]*(cegcount-i))/cegcount
		local z1 = (z*i + goal[3]*(cegcount-i))/cegcount
		spSpawnCEG(teleportDef.ceg or TELEPORT_CEG, x1, y1, z1)
	end
	
	
	Spring.SetUnitPosition(unitID, goal[1], goal[2], goal[3] )
	local oldQueue = spGetCommandQueue(unitID)
	ReloadQueue(unitID, oldQueue, cmdTag)
	StartScript(Reloadloop)
	teleporting[unitID] = false
	return true
end

-- a bit convoluted for this but might be           
-- useful for lua unit scripts
local function UpdateCoroutines() 
  local newCoroutines = {} 
  for i=1, #coroutines do 
    local co = coroutines[i] 
    if (coroutine.status(co) ~= "dead") then 
      newCoroutines[#newCoroutines + 1] = co 
    end 
  end 
  coroutines = newCoroutines 
  for i=1, #coroutines do 
    assert(coroutine.resume(coroutines[i]))
  end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

function gadget:Initialize()
  Spring.SetCustomCommandDrawData(CMD_TELEPORT, "Attack", {0, 1, 0, 1})
  gadgetHandler:RegisterCMDID(CMD_TELEPORT)
  for _, unitID in pairs(Spring.GetAllUnits()) do
    gadget:UnitCreated(unitID, Spring.GetUnitDefID(unitID))
  end
end


function gadget:UnitCreated(unitID, unitDefID, unitTeam)
  if (not teleportDefs[unitDefID]) then
    return
  end 
  local t = spGetGameSeconds()
  lastTeleport[unitID] = t
  spInsertUnitCmdDesc(unitID, teleportCmdDesc)
end


function gadget:UnitDestroyed(unitID, unitDefID)
  lastTeleport[unitID]  = nil
end


function gadget:AllowCommand(unitID, unitDefID, teamID,
                             cmdID, cmdParams, cmdOptions)
  if (cmdID == CMD_TELEPORT and 
      spTestBuildOrder(
          unitDefID, cmdParams[1], cmdParams[2], cmdParams[3], 1) == 0) then
      return false
  end
  return true -- allowed
end


function gadget:CommandFallback(unitID, unitDefID, teamID,    -- keeps getting 
                                cmdID, cmdParams, cmdOptions) -- called until
  if (cmdID ~= CMD_TELEPORT)or(not teleportDefs[unitDefID]) then      -- you remove the
    return false  -- command was not used                     -- order
  end

  local x, y, z = spGetUnitBasePosition(unitID)
  local distSqr = GetDist2Sqr({x, y, z}, cmdParams)
  local teleportDef 	= teleportDefs[unitDefID]
  local range   		= teleportDef.range
  local reload  		= teleportDef.reload or 0
  local t       		= spGetGameSeconds()

  if (teleporting[unitID]) then
	return true, false -- command was used but don't remove it
  end
  
  if (distSqr < (range*range)) then
    local cmdTag = spGetCommandQueue(unitID,1)[1].tag
    if ((t - lastTeleport[unitID]) >= reload) then
      local coords = table.concat(cmdParams)
      if (not teleports[coords]) then
        if (not Teleport(unitID, cmdParams, cmdTag, x, y, z)) then
          return true, true -- command was used, remove it 
        end
        teleports[coords] = 1
        return true, false -- command was used, remove it 
      else
	    local r = landBoxSize*teleports[coords]^0.5/2
        local randpos = {
          cmdParams[1] + random(-r, r),
          cmdParams[2],
          cmdParams[3] + random(-r, r)}
        if (not Teleport(unitID, randpos, cmdTag, x, y, z)) then
          return true, true -- command was used, remove it 
        end
        teleports[coords] = teleports[coords] + 1
        return true, false -- command was used, remove it 
      end
    end
  else
    Approach(unitID, cmdParams, range)
  end
  
  return true, false -- command was used but don't remove it
end

function gadget:GameFrame(n)
  UpdateCoroutines()
end
